# This file is part of the Lumenn project 
# at http://code.google.com/p/lumenn/
#
# Released under the GPLv3 at http://www.gnu.org/copyleft/gpl.html
# Designed for pygtk and the Maemo platform.
#
# Copyright 2008 by Jackson Yee (jackson@gotpossum.com)

import pygtk
pygtk.require('2.0')

import gtk
import gtk.gdk as gdk
import gobject
import cairo
import pango
import time
import gc
import lumenn.files
import os
import os.path
import threading
import gobject
import time
import math
import traceback
import sys

import lumenn.log

log	=	lumenn.log.log

(	USING_CALLBACK,
)	=	range(1, 2)

# ====================================================================
def ResizeImg(img, width, height):
	# Scale to fit window
	w = img.get_width()
	h = img.get_height()
				
	ratio = float(w) / h
	thumbratio = float(width) / height
			
	# Image is wider than taller
	if ratio > thumbratio:
		magnification = float(width) / w
	# Image is taller
	else:
		magnification = float(height) / h
					
	nw = int(w * magnification)
	nh = int(h * magnification)
				
	if nw == 0:
		nw = 1
				
	if nh == 0:
		nh = 1
		
	method = gdk.INTERP_NEAREST
	
	if nw >= 800 or nh >= 800:
		method = gdk.INTERP_BILINEAR
				
	return img.scale_simple(nw, nh, method)

# *********************************************************************
class Cache(gobject.GObject):
	"""Asynchronous image cache supporting automatic size allocation,
	picture resizing, and thumbnail creation.
	
	Thumbnails will be cached in the directory that the image 
	is located within under a .thumbnails subdirectory.
	"""
	# -------------------------------------------------------------------
	def __init__(self, size = 1, scaling = None, async = True):
		gobject.GObject.__init__(self)
		self.size = size
		self.images = {}
		self.scaling = scaling
		self.filestoload = []
		self.async	=	async

	# -------------------------------------------------------------------
	def Load(self, filename):
		if filename not in self.images.keys():
			
			if self.async:
				if filename in self.filestoload:
					return USING_CALLBACK

			self.filestoload.append(filename)

			if len( self.images ) >= self.size:
				# Find the oldest image and delete it
				oldest = None
				oldesttime = time.time()

				for k, v in self.images.iteritems():
					if v[1] < oldesttime:
						oldesttime = v[1]
						oldest = k

				del self.images[oldest]
				gobject.idle_add( gc.collect )
				
			if self.async:
				gobject.idle_add(self.LoadOnIdle)
				return USING_CALLBACK
			else:
				return self.LoadOnIdle()
		else:
			self.images[filename][1] = time.time()
			return self.images[filename][0]

	# -------------------------------------------------------------------
	def __getitem__(self, filename):
		return self.Load(filename)

	# -------------------------------------------------------------------
	def HasThumb(self, fullpath):
		dirname, filename = os.path.split(fullpath)
		
		thumbname = os.path.join(dirname,
			'.thumbnails',
			filename,
		) 
		
		filetime	= os.stat(fullpath).st_mtime
		
		try:
			thumbtime	= os.stat(thumbname).st_mtime
		except:
			thumbtime = 0
					
		if filetime < thumbtime:
			return True
			
		return False
		
	# -------------------------------------------------------------------
	def LoadOnIdle(self):
		if not self.filestoload:
			return

		fullpath = self.filestoload.pop(0)
		dirname, filename	=	os.path.split(fullpath)
		
		thumbname = os.path.join(dirname,
			'.thumbnails',
			filename,
		) 
		
		filetime	= os.stat(fullpath).st_mtime
		
		try:
			thumbtime	= os.stat(thumbname).st_mtime
		except:
			thumbtime = 0
					
		try:
			os.mkdir( os.path.join(dirname, '.thumbnails') )
		except Exception, e:
			pass

		if self.scaling and filetime < thumbtime:
			try:
				img = gdk.pixbuf_new_from_file(thumbname)
				
				self.images[fullpath] = [img, time.time()]
	
				gobject.idle_add( self.emit, 'image_loaded', fullpath, self.images[fullpath][0])	

				if self.async:
					if self.filestoload:
						gobject.idle_add(self.LoadOnIdle)
				return
			except Exception, e:
				print e

		try:
			img = gdk.pixbuf_new_from_file(fullpath)

			if self.scaling:
				img = ResizeImg(img,
					self.scaling[0],
					self.scaling[1],
				)
				
				gobject.idle_add( gc.collect )
				img.save( thumbname,
					'jpeg',
					{
						'quality'	:	'70',
					},
				)

			self.images[fullpath] = [img, time.time()]
		except Exception, e:
			log.write( 'Cache: Unable to load file %s: %s' % (fullpath, e) )
			img = gdk.Pixbuf(gdk.COLORSPACE_RGB, 
					False,
					8,
					8, 
					8,
				) 
			self.images[fullpath] = [img, time.time()]

		if self.async:
			gobject.idle_add( self.emit, 'image_loaded', fullpath, self.images[fullpath][0])				
			if self.filestoload:
				gobject.idle_add(self.LoadOnIdle)
		
		return img

# *********************************************************************
gobject.signal_new("image_loaded", 
	Cache,
	gobject.SIGNAL_ACTION,
	gobject.TYPE_NONE,
	(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, ) )

# *********************************************************************
class ImageBase(gtk.DrawingArea):

	# -------------------------------------------------------------------
	def __init__(self):
		gtk.DrawingArea.__init__(self)

	# -------------------------------------------------------------------
	def DrawStatus(self, status):
		wnd = self.window

		if not wnd:
			return
			
		dw, dh = wnd.get_size()

		if dw <= 100 or dh <= 100:
			return

		ctx = wnd.cairo_create()
		ctx.set_source_rgb(0, 0, 0)
		ctx.rectangle(0, dh - 28, dw, dh)
		ctx.fill()
		ctx.set_source_rgb(0, 0xff, 0)
		ctx.select_font_face('Sans', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
		ctx.set_font_size(14)
		ctx.move_to(10, dh - 14)
		ctx.show_text(status)

	# -------------------------------------------------------------------
	def Emit(self, signal, arg1, arg2):
		self.emit(signal, arg1, arg2)

gobject.type_register(ImageBase)

# *********************************************************************
class Image(ImageBase):
	
	postimageloaded	=	None
	
	# -------------------------------------------------------------------
	def __init__(self, cache = None):
		ImageBase.__init__(self)

		self.cache	= cache
		self.img	=	None
		self.rotation	=	0
		self.fittowindow	=	True
		self.Clear()

		self.connect("expose_event", self.OnExpose)
		self.connect("button_press_event", self.OnPress)
		self.connect("button_release_event", self.OnRelease)
		self.connect("motion_notify_event", self.OnMotion)
		self.connect("configure_event", self.OnConfigure)
		
		if self.cache:
			self.cache.connect("image_loaded", self.OnImageLoaded)

		self.set_events(gtk.gdk.EXPOSURE_MASK
			| gtk.gdk.BUTTON_PRESS_MASK
			| gtk.gdk.BUTTON_RELEASE_MASK
			| gtk.gdk.POINTER_MOTION_MASK
			| gtk.gdk.POINTER_MOTION_HINT_MASK)

	# -------------------------------------------------------------------
	def OnExpose(self, widget, event):
		wnd = self.window
		dw, dh = wnd.get_size()

		if dw < 0 or dh < 0:
			return

		ctx = wnd.cairo_create()

		ctx.rectangle( (0, 0, dw, dh) )
		ctx.clip_preserve()

		ctx.set_source_rgb(0, 0, 0)
		ctx.rectangle( (0, 0, dw, dh) )
		ctx.fill()

		ctx.set_source_rgb(0, 1, 0)
		ctx.select_font_face('Sans', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
		ctx.set_font_size(14)

		if self.backtext and dw > 30:
			ctx.move_to(10, 14)
			ctx.show_text(self.backtext)

		if not self.img or self.img == USING_CALLBACK:
			return False

		ctx.save()	
		
		ctx.set_source_pixbuf(self.img, self.x, self.y)
		ctx.paint()
		ctx.restore()
		
		if self.postimageloaded:
			self.postimageloaded(self)

		if self.statustext:
			ctx.move_to(10, 14)
			ctx.show_text(self.statustext)
		return False

	# -------------------------------------------------------------------
	def OnMotion(self, widget, event):
		if not self.indrag:
			return False

		if event.is_hint:
			x, y, state = event.window.get_pointer()
		else:
			x, y, state = event.x, event.y, event.state			
		self.x -= (self.lastx - x) * 4
		self.y -= (self.lasty - y) * 4
		self.lastx, self.lasty = x, y
		self.queue_draw()
		return False

	# -------------------------------------------------------------------
	def OnPress(self, widget, event):
		if not self.filename:
			return False

		if event.button == 1:
			self.indrag = True
			self.lastx, self.lasty = event.x, event.y

		return True

	# -------------------------------------------------------------------
	def OnRelease(self, widget, event):
		if event.button == 1:
			self.indrag = False

		return True

	# -------------------------------------------------------------------
	def OnConfigure(self, widget, event):
		if self.filename:
			self.Load(self.filename)
		return True

	# -------------------------------------------------------------------
	def Load(self, filename):
		if not self.window:
			return

		self.filename = filename
		self.x = 0
		self.y = 0
		self.rotation	=	0

		if self.cache:
			self.DrawStatus('Loading %s' % filename)

		try:
			if self.cache:
				self.img = self.cache.Load(filename)
			else:
				self.img = gtk.gdk.pixbuf_new_from_file(filename)
		except Exception, e:
			log.write( 'Image: Unable to load file %s: %s' % (self.filename, e) )
			self.DrawStatus('Unable to load %s: %s' % (filename, e) )

		if self.img == USING_CALLBACK:
			return

		self.OnImageLoaded(self.cache, filename, self.img)

	# -------------------------------------------------------------------
	def OnImageLoaded(self, cache, filename, img):
		if self.filename != filename:
			return
		
		# Scale to fit window
		w, h = img.get_width(), img.get_height()
		ww, wh = self.window.get_size()
		
		self.origw	=	w
		self.origh	=	h

		if self.fittowindow:
			ratio = float(w) / h
			windowratio = float(ww) / wh

			# Image is wider than taller
			if ratio > windowratio:
				self.magnification = float(ww) / w
			# Image is taller
			else:
				self.magnification = float(wh) / h
		
		if self.magnification < 0.99 or self.magnification > 1.01:
			try:
				nw = int(w * self.magnification)
				nh = int(h * self.magnification)
				
				method	=	gdk.INTERP_NEAREST
				
				if nw >= 800 or nh >= 800:
					method = gdk.INTERP_BILINEAR
				self.img	=	img.scale_simple(nw, nh, method)
			except Exception, e:
				self.DrawStatus('Could not load %s: %s' % (self.filename, e) )
		else:
			self.img = img

		self.x	= ww / 2 - int(w * self.magnification / 2)
		self.y	= wh / 2 - int(h * self.magnification / 2)

		self.queue_draw()

	# -------------------------------------------------------------------
	def OriginalSize(self):
		self.magnification = 1.0
		self.img = self.cache.Load(self.filename)
		self.queue_draw()		
		
	# -------------------------------------------------------------------
	def Zoom(self, factor):
		if self.img == USING_CALLBACK:
			return		
		
		factor	=	float(factor)

		# Readjust x and y so that the center of the viewport stays
		# on the same part of the picture while zooming in and out
		pw, ph = self.img.get_width(), self.img.get_height()
		ww, wh = self.window.get_size()

		percentw	= ((float(ww) / 2) - self.x) / pw
		percenth	= ((float(wh) / 2) - self.y) / ph

		async = self.cache.async
		self.cache.async	=	False
		img = self.cache.Load(self.filename)
		self.cache.async	=	async
		
		if self.rotation:
			img = img.rotate_simple(self.rotation)

		self.magnification *= factor
		npercentw	=	percentw * factor
		npercenth	=	percenth * factor

		self.x	+=	(percentw - npercentw) * pw
		self.y	+=	(percenth - npercenth) * ph

		pw, ph = img.get_width(), img.get_height()

		nw = int(pw * self.magnification)
		nh = int(ph * self.magnification)
				
		method	=	gdk.INTERP_NEAREST
				
		if nw >= 800 or nh >= 800:
			method = gdk.INTERP_BILINEAR
			
		self.img	=	img.scale_simple(nw, nh, method)

		gobject.idle_add( gc.collect )
		self.queue_draw()

	# -------------------------------------------------------------------
	def Rotate(self, angle):
		self.rotation += angle
		
		if angle == 90:
			self.img = self.img.rotate_simple(gdk.PIXBUF_ROTATE_COUNTERCLOCKWISE)
		else:
			self.img = self.img.rotate_simple(gdk.PIXBUF_ROTATE_CLOCKWISE)
			
		t = self.x
		self.x = self.y
		self.y = t
		
		gobject.idle_add( gc.collect )
		
		self.queue_draw()
		
	# -------------------------------------------------------------------
	def Clear(self):
		self.filename = None
		self.pixbuf = None
		self.magnification = 1.0
		self.x, self.y = 0, 0
		self.lastx, self.lasty = 0, 0
		self.indrag = False
		self.backtext = None
		self.statustext = None
		gc.collect()
		
	# -------------------------------------------------------------------
	def Save(self, filename):
		if not self.img:
			return
			
		self.img.save(filename, 
			'jpeg', 
			{ 
				'quality'	:	'90',
			},
		)
		
	# -------------------------------------------------------------------
	def GetDimensions(self):
		return self.origw, self.origh

# *********************************************************************
class Thumbnail(ImageBase):

	# -------------------------------------------------------------------
	def __init__(self, image = None, cache = Cache()):
		ImageBase.__init__(self)

		self.image = image
		self.cache = cache
		self.Clear()

		self.connect("expose_event", self.OnExpose)
		self.connect("button_press_event", self.OnPress)
		self.connect("button_release_event", self.OnRelease)
		self.connect("motion_notify_event", self.OnMotion)
		self.cache.connect("image_loaded", self.OnImageLoaded)

		self.set_events(gtk.gdk.EXPOSURE_MASK
			| gtk.gdk.BUTTON_PRESS_MASK
			| gtk.gdk.BUTTON_RELEASE_MASK
			| gtk.gdk.POINTER_MOTION_MASK
			| gtk.gdk.POINTER_MOTION_HINT_MASK)

	# -------------------------------------------------------------------
	def __len__(self):
		return len(self.filelist)

	# -------------------------------------------------------------------
	def __getitem__(self, pos):
		return self.filelist[pos]

	# -------------------------------------------------------------------
	def __delitem__(self, k):
		del self.filelist[k]

		self.queue_draw()

	# -------------------------------------------------------------------
	def OnMotion(self, widget, event):
		if not self.indrag:
			return False

		if event.is_hint:
			x, y, state = event.window.get_pointer()
		else:
			x, y, state = event.x, event.y, event.state

		self.momentumy = self.lasty - y
		self.lasty = y
		self.viewy += self.momentumy * 4

		if self.viewy < 0:
			self.viewy = 0

		if self.viewy > self.maxy - self.dh:
			self.viewy = self.maxy - self.dh

		self.queue_draw()
		return False

	# -------------------------------------------------------------------
	def OnPress(self, widget, event):
		if not len(self.filelist):
			return False

		if event.type == gtk.gdk.BUTTON_PRESS:
			if event.button == 1:
				self.indrag = True
				self.lastx, self.lasty = event.x, event.y

				self.clickx	=	event.x
				self.clicky	=	event.y

		return True

	# -------------------------------------------------------------------
	def OnRelease(self, widget, event):
		if event.button == 1:
			self.indrag = False
			x, y, state = event.x, event.y, event.state
			#self.momentumy = self.lasty - y
			#print 'Released.	Lasty:	%d	y:	%d' % (self.lasty, y)
			#print 'Momentumy:	%d'	% self.momentumy

			#if self.viewy > 0:
			#	self.viewy = 0

			self.lasty = 0

			dx = self.clickx - x
			dy = self.clicky - y

			if dx >= -16 and dx <= 16 and dy >= -16 and dy <= 16:
				self.OnLeftClick(widget, event)

			#gobject.timeout_add(200, self.OnMomentum)

		return True

	# -------------------------------------------------------------------
	def OnLeftClick(self, widget, event):
		if not self.rows or not self.cols:
			return

		x, y = event.x, event.y		
		pos = int( (self.viewy + y) / self.th)

		if self.cols > 1:
			pos *= self.cols
			pos += int( x / self.tw )

		self.SetPos(pos)
		self.emit('selected', pos)

	# -------------------------------------------------------------------
	def OnExpose(self, widget, event):
		wnd = self.window
		dw, dh = wnd.get_size()

		if dw <= 0 or dh <= 0:
			return

		self.dh	=	dh

		ctx = wnd.cairo_create()

		ctx.rectangle( (0, 0, dw, dh) )
		ctx.clip_preserve()

		ctx.set_source_rgb(0, 0, 0)
		ctx.fill()

		b = self.border
		l = len(self.filelist)

		tw, th = self.cache.scaling
		tw += b * 2
		th += b * 2

		self.tw = tw
		self.th	=	th
		self.rows = int(dh / th)
		self.cols = int(dw / tw)

		if not self.rows or not self.cols:
			return

		pos = int( self.viewy / th )

		if self.cols > 1:
			pos *= self.cols

		x = 0
		y = self.viewy % th

		if y > 0:
			y = -y

		self.maxy	=	int( l / self.cols * th)

		stopy = y + dh + th

		#	y Start:	0
		# y	End:		pos / self.cols * self.rows

		#print	'Number of images:	%d'	% l
		#print 'Pos:	%d	YView:	%d	YStart:	0	YEnd:	%d' % (pos, self.viewy, pos / self.cols * self.rows)

		keepgoing = True

		#possumlib.utils.applog.write( 'Pos calculated to be %d, y calculated to be %d' % (pos, y) )

		self.viewposstart	=	pos
		self.thumbrects = []

		while keepgoing:
			if pos >= l:
				break
				
			img	=	USING_CALLBACK

			try:				
				img = self.cache.Load( self.filelist[pos].GetPath() )
			except Exception, e:
				log.write( 'Thumbnail: Unable to load file %s: %s' % (self.filelist[pos], e) )
				self.DrawStatus( str(e) )
				continue 

			if pos == self.selected:
				ctx.set_source_rgb(0.2, 0.2, 1)
				ctx.rectangle(x, y, tw, th)
				ctx.fill()

			thisx = x + b
			thisy = y + b

			ctx.set_font_size(14)

			if img == USING_CALLBACK:
				ctx.set_source_rgb(0.3, 0.3, 0.3)
				ctx.rectangle(thisx, thisy, tw - 2 * b, th - 2 * b)				
				ctx.fill()
				ctx.move_to(x + b, y + th / 2)
				ctx.set_source_rgb(0, 1, 0)
				ctx.show_text('Loading')

				pos += 1
				x += tw

				if x + tw > dw:
					x = 0
					y += th

				if y > stopy:
					keepgoing = False

				#print 'Y is %d, dh is %d' % (y, dh)
				continue

			w, h = img.get_width(), img.get_height()
			
			try:
				ctx.set_source_pixbuf(img, 
					thisx + ( tw - 2 * b )/ 2 - w / 2, 
					thisy + (th - 2 * b)/ 2 - h / 2,
				)
				ctx.paint()			
			except Exception, e:
				log.write('Image: Trouble drawing rgb image: %s' % e)
			
			pos += 1
			x += tw

			if x + tw > dw:
				x = 0
				y += th

			if y > stopy:
				keepgoing = False					

			#print 'Y is %d, dh is %d' % (y, dh)

		if pos < l:
			preload = l - pos

			if preload > 5:
				preload = 5

			#for i in range(pos + 1, pos + preload):
			#	self.cache.Load(self.filelist[i])

		self.viewposstop	=	pos

		return False

	# -------------------------------------------------------------------
	def OnImageLoaded(self, cache, filename, img):
		l = len(self.cache.filestoload)

		if l:
			self.DrawStatus('%s left to load...' % l)
		else:
			self.DrawStatus('Finish loading')

		self.queue_draw()

	# -------------------------------------------------------------------
	def OnMomentum(self):
		if self.indrag:
			return False

		if self.momentumy >= 0:
			#print 'Momentum:	%d'	% self.momentumy
			self.viewy -=	self.momentumy

			if self.momentumy > 2:
				self.momentumy -= 1
				gobject.timeout_add(25, self.OnMomentum)
			elif self.momentumy < -2:
				self.momentumy += 1
				gobject.timeout_add(25, self.OnMomentum)
			else:
				self.momentumy = 0

			if self.viewy > 0:
				self.viewy = 0				

			self.queue_draw()

	# -------------------------------------------------------------------
	def Clear(self):
		self.filelist = lumenn.files.FileList()
		self.selected 	= 0
		self.viewy 			= 0
		self.momentumy	=	0
		self.maxy				=	0
		self.dh					=	0
		self.clickx			=	0
		self.clicky			=	0
		self.viewposstart	=	0
		self.viewposstop	=	0
		self.lastx, self.lasty = 0, 0
		self.border = 2
		self.rows = 0
		self.cols = 0
		self.tw		=	0
		self.th		=	0
		self.indrag = False

		gc.collect()

	# -------------------------------------------------------------------
	def GetPos(self):
		return self.selected

	# -------------------------------------------------------------------
	def SetPos(self, pos):
		self.DrawStatus('Loading...')

		wnd = self.window
		dw, dh = wnd.get_size()

		self.selected = pos

		if self.rows and self.cols:
			if pos <= self.viewposstart or pos >= self.viewposstop - self.cols * 2:
				newviewy = pos / self.cols * self.th
				if newviewy > self.viewy + dh or newviewy < self.viewy:
					self.viewy = newviewy

				if self.viewy > self.maxy - self.dh:			
					self.viewy = self.maxy - self.dh

		self.queue_draw()

# *********************************************************************
gobject.signal_new("selected", 
	Thumbnail,
	gobject.SIGNAL_ACTION,
	gobject.TYPE_NONE,
	(gobject.TYPE_PYOBJECT,) )

